# -*- coding: utf8 -*-
"""
    bt 客户端
"""

import os
import sys
import json
import time
import socket
import random
import urlparse
import copy
import pickle
import collections
import libtorrent as lt
import threading
import requests
from multiprocessing import Pipe
from functools import reduce
from optparse import OptionParser
from cent import generate_token

import logging
logger = logging.getLogger('bit_torrent')
from base.settings import (DAEMON_BT_CLIENT_DAEMON_PID_FILE,
                           BIT_TORRENT_SETTINGS, WEBSOCKET_CLIENT_URL,
                           WEBSOCKET_SECRET, FOLDER_CATEGORY_PATH_DICT,
                           TORRENT_FOLDER_ROOT, WEBSOCKET_CHANNLE,
                           TORRENT_FILE_VALID_TIME, TORRENT_SUFFIX)
from common.daemon import Daemon
from common.centrifuge import Credentials, CentrifugeClient
from common.net_utils import get_mac_address, get_wan_ips
from common import pystun
from .exceptions import BitTorrentError


TORRENT_FOLDER_ROOT = os.path.abspath(TORRENT_FOLDER_ROOT)

VERSION = 1.0
CLIENT_INFO = 'bt_client:{} libtorrent:{}'.format(VERSION, lt.version)

def add_suffix(val):
    prefix = ['B', 'kB', 'MB', 'GB', 'TB']
    for i in range(len(prefix)):
        if abs(val) < 1000:
            if i == 0:
                return '%3.3g%s' % (val, prefix[i])
            else:
                return '%2.3g%s' % (val, prefix[i])
        val /= 1000

    return '%6.3gPB' % val


class BtClientDaemon(Daemon):
    state_str = [
        'queued', 'checking', 'downloading metadata', 'downloading',
        'finished', 'seeding', 'allocating', 'checking fastresume'
    ]
    # 默认libtorrent设置 参考 https://www.libtorrent.org/reference-Settings.html#overview
    default_settings = {
        'user_agent': CLIENT_INFO,
        'alert_mask': lt.alert.category_t.all_categories,
        'listen_system_port_fallback': False,  # 当端口绑定失败 禁止使用系统默认分配的端口
    }

    @classmethod
    def get_torrent_file_path(cls, file_path, torrent_folder):
        """获取文件对应的torrent文件
        
        Arguments:
            file_path {[type]} -- [description]
            torrent_folder {[type]} -- [description]
        
        Returns:
            string -- torrent文件
        """

        torrent_folder = os.path.abspath(torrent_folder)
        file_name = os.path.split(file_path)[-1]
        torrent_file_name = file_name + '.torrent'
        torrent_file_path = os.path.join(torrent_folder, torrent_file_name)

        return torrent_file_path

    def __init__(self):
        super(BtClientDaemon, self).__init__(
            name='Bt客户端进程', pid_file=DAEMON_BT_CLIENT_DAEMON_PID_FILE)
        self._net_interface = None
        self._net_interface_port = 7888
        self._settings = {}
        self._torrents = collections.OrderedDict()
        self._tracker_urls = []
        self._session = None
        self.settings = BIT_TORRENT_SETTINGS
        self.centrifuge_client = None
        self._nat_type = 'Unkonw'
        self._external_ip = None
        self._external_port = None

    @property
    def settings(self):
        return self._settings

    @settings.setter
    def settings(self, value):
        # 参考 https://www.libtorrent.org/reference-Settings.html#overview
        self._settings = copy.deepcopy(self.default_settings)
        # 设置本地监听地址
        if value.get('listen_interfaces'):
            self._net_interface = value['listen_interfaces'].split(':')[0]
            self._net_interface_port = int(
                value['listen_interfaces'].split(':')[1])

        # 设置最大上传速度
        if value.get('upload_rate_limit', -1) > 0:
            self._settings[
                'upload_rate_limit'] = value['upload_rate_limit'] * 1024

        # 设置最大下载速度
        if value.get('download_rate_limit', -1) > 0:
            self._settings[
                'download_rate_limit'] = value['download_rate_limit'] * 1024

        # 设置最大链接数
        if value.get('connections_limit'):
            self._settings['connections_limit'] = value['connections_limit']

        # 设置tracker_backoff
        if value.get('tracker_backoff'):
            self._settings['tracker_backoff'] = value['tracker_backoff']

        # 设置 http 代理
        if value.get('proxy_host'):
            self._settings['proxy_type'] = lt.proxy_type_t.http
            self._settings['proxy_hostname'] = value['proxy_host'].split(':')[
                0]
            self._settings['proxy_port'] = value['proxy_host'].split(':')[1]

        # 设置 tracker urls
        if value.get('tracker_urls'):
            self._tracker_urls = value['tracker_urls']

    def _add_torrent(self, magnet_or_torrent_file_path, file_save_folder):
        """添加 磁力链接/torrent
        
        Arguments:
            magnet_or_torrent_file_path {string} -- 磁力链接/torrent文件位置
            file_save_folder {string} -- 文件所在目录
        """

        atp = {}
        # 磁力链接
        if magnet_or_torrent_file_path.startswith('magnet:'):
            atp = lt.parse_magnet_uti(magnet_or_torrent_file_path)
        # torrent种子
        else:
            try:
                torrent = lt.bdecode(
                    open(magnet_or_torrent_file_path, 'rb').read())
                torrent_info = lt.torrent_info(torrent)
                atp['ti'] = torrent_info
                atp['resume_data'] = open(
                    os.path.join(file_save_folder,
                                 torrent_info.name() + '.fastresume'), 'rb').read()
            except:
                pass

        atp['save_path'] = file_save_folder
        atp['storage_mode'] = lt.storage_mode_t.storage_mode_sparse
        atp['paused'] = False
        atp['auto_managed'] = True
        atp['duplicate_is_error'] = True
        self._session.async_add_torrent(atp)
        # handle = self._session.add_torrent(atp)

    def _remove_torrent(self, file_path):
        """移除 torrent
        
        Arguments:
            file_path {stirng} -- 文件位置
        """
        file_path = os.path.abspath(file_path)
        for key, handle in self._torrents.iteritems():
            _file_path, _ = key
            if file_path == _file_path:
                self._session.remove_torrent(handle)
                # logger.debug('remove_torrent: {}'.format(file_path))
                break

    def _print_status(self, status):
        out = '{:<40} [{}] '.format(status.name[:40], status.active_time)
        out += '{}: {:.2f} '.format(self.state_str[status.state],
                                    status.progress * 100)

        if status.state != lt.torrent_status.seeding:
            out += 'total_done: {} '.format(add_suffix(status.total_done))
            out += 'peers: {} seeds: {} distributed_copies: {} '.format(
                status.num_peers, status.num_seeds, status.distributed_copies)

        out += 'down: {}/s ({}) up: {}/s ({})'.format(
            add_suffix(status.download_rate),
            add_suffix(status.total_download), add_suffix(status.upload_rate),
            add_suffix(status.total_upload))

        # if status.state != lt.torrent_status.seeding:
        #     out += 'info-hash: %s\n' % status.info_hash
        #     out += 'next announce: %s\n' % status.next_announce
        #     out += 'tracker: %s\n' % status.current_tracker
        logger.info(out)

    def _print_peer_info(self, peers):
        out = '         addr            down  ( total )   up   ( total )  q  r flags      progress    client\n'

        for peer in peers:
            out += '%-15s:%-5s ' % peer.ip
            out += '%s/s ' % add_suffix(peer.down_speed)
            out += '(%s) ' % add_suffix(peer.total_download)
            out += '%s/s ' % add_suffix(peer.up_speed)
            out += '(%s) ' % add_suffix(peer.total_upload)
            out += '%2d ' % peer.download_queue_length
            out += '%2d ' % peer.upload_queue_length

            out += 'I' if peer.flags & lt.peer_info.interesting else '.'
            out += 'C' if peer.flags & lt.peer_info.choked else '.'
            out += 'i' if peer.flags & lt.peer_info.remote_interested else '.'
            out += 'c' if peer.flags & lt.peer_info.remote_choked else '.'
            out += 'e' if peer.flags & lt.peer_info.supports_extensions else '.'
            out += 'l' if peer.flags & lt.peer_info.local_connection else 'r'
            out += ' '

            if peer.downloading_piece_index >= 0:
                assert (peer.downloading_progress <= peer.downloading_total)
                out += '%s(%s) ' % (peer.downloading_progress,
                                    peer.downloading_total)

            if peer.flags & lt.peer_info.handshake:
                id = 'waiting for handshake'
            elif peer.flags & lt.peer_info.connecting:
                id = 'connecting to peer'
            else:
                id = peer.client

            out += '%s\n' % id[:10]

        logger.debug(out)

    def _check_nat(self):
        """
        检测nat类型
        """

        logger.info('检测Nat类型...')
        self._nat_type, self._external_ip, self._external_port = pystun.get_ip_info(
            source_ip='0.0.0.0', source_port=self._net_interface_port)
        logger.info('Nat: {}'.format(self._nat_type))
        logger.info('External IP: {}'.format(self._external_ip))
        logger.info('Local Port: {}  External Port: {}'.format(
            self._net_interface_port, self._external_port))

    def _add_valid_torrent_file_from_torrent_folder(self):
        """
        从 TORRENT_FOLDER_ROOT 文件夹下加载有效的torrent文件
        """

        valid_time = time.time() - TORRENT_FILE_VALID_TIME
        for root, dirs, files in os.walk(TORRENT_FOLDER_ROOT):
            category = os.path.split(root)[-1]
            if category not in FOLDER_CATEGORY_PATH_DICT:
                continue
            logger.info('加载 "{}" 文件夹下的 torrent 文件'.format(root))
            for file in files:
                if not file.endswith(TORRENT_SUFFIX):
                    continue
                file_path = os.path.join(root, file)
                last_modfiy_timestemp = os.path.getmtime(file_path)
                if last_modfiy_timestemp < valid_time:
                    logger.info('> {} 已过期!'.format(file_path))
                    continue
                logger.info('> {} 有效.'.format(file_path))
                resource_save_folder = FOLDER_CATEGORY_PATH_DICT[category]
                self._add_torrent(file_path, resource_save_folder)

    def _handle_resource_download(self, uid, channel, data, **kwargs):
        try:
            torrent_file_path = data['torrent_file_path']
            file_path = data['file_path']
            torrent_file_name = data['torrent_file_name']
            file_name = data['file_name']
            category = data['category']
        except KeyError as e:
            logger.warning('缺少必要参数, 忽略! {}'.format(e))
            return

        folder = FOLDER_CATEGORY_PATH_DICT.get(category)
        if not folder:
            logger.warning('为设置"{}"类型文件的存储位置: 参考: FOLDER_CATEGORY_PATH_DICT'.format(category))
            return

        torrent_foloder = os.path.join(TORRENT_FOLDER_ROOT, category)
        if not os.path.isdir(torrent_foloder):
            os.makedirs(torrent_foloder)
        logger.info('开始下载: {}'.format(torrent_file_name))
        torrent_save_path = os.path.join(torrent_foloder, torrent_file_name)
        try:
            r = requests.get(torrent_file_path)
            # logger.info(r)
            with open(torrent_save_path, 'wb') as f:
                f.write(r.content)
        except Exception as e:
            logger.warning('下载/保存失败: {}'.format(e))
            return

        # 下载文件
        self._add_torrent(torrent_save_path, folder)

    def _handle_resource_query(self, uid, channel, data, **kwargs):
        try:
            name = data['name']
            info_hash = data['info_hash']
        except KeyError as e:
            logger.warning('缺少必要参数, 忽略! {}'.format(e))
            return

        for key, handle in self._torrents.iteritems():
            _file_path, _info_hash = key
            file_name = os.path.split(_file_path)[-1]
            if info_hash == _info_hash and name == file_name:
                status = handle.status()
                peers = handle.get_peer_info()
                l = []
                for peer in peers:
                    l.append({
                        'addr': '%-15s:%-5s ' % peer.ip,
                    })
                self.centrifuge_client.send(
                    'publish', {
                        'channel': '{}:broadcast'.format(WEBSOCKET_CHANNLE),
                        'data': {
                            'msg_type': 'WHO_HAS_THIS_RESOURE_ANSWER',
                            'name': name,
                            'info_hash': info_hash,
                            'state': self.state_str[status.state],      # 状态
                            'progress': status.progress,                # 完成度
                            'total_download': status.total_download,    # 总下载量
                            'total_upload': status.total_upload,        # 总上传量
                            'peers': l,                                 # 当前peers
                        }
                    }, uid)
                break;

    def _ws_personnel_message_handle(self, uid, channel, data, **kwargs):
        """
        websocket 专属频道消息处理
        """

        try:
            msg_type = data['msg_type']
        except KeyError as e:
            logger.warning('错误的数据格式, 忽略!')
            return

        # 下载资源
        if msg_type == 'DOWNLOAD_RESOURCE':
            self._handle_resource_download(uid, channel, data, **kwargs)
        else:
            logger.warning('未知"msg_type": "{}"'.format(msg_type))

    def _ws_broadcast_message_handle(self, uid, channel, data, **kwargs):
        """
        websocket 广播频道消息处理
        """

        if self.centrifuge_client.client_id == kwargs.get('client', ''):
            return
        try:
            msg_type = data['msg_type']
        except KeyError as e:
            logger.warning('错误的数据格式, 忽略!')
            return

        # 下载资源
        if msg_type == 'DOWNLOAD_RESOURCE':
            self._handle_resource_download(uid, channel, data, **kwargs)
        # 资源查询
        elif msg_type == 'WHO_HAS_THIS_RESOURE':
            self._handle_resource_query(uid, channel, data, **kwargs)
        else:
            logger.warning('未知"msg_type": "{}"'.format(msg_type))

    def _monitor_websocket(self):
        user = get_mac_address()
        # 专属频道
        personnel_channel_name = '{}:{}'.format(WEBSOCKET_CHANNLE, user)
        # 广播通道
        broadcast_channel_name = '{}:broadcast'.format(WEBSOCKET_CHANNLE)
        timestamp = str(int(time.time()))

        info = json.dumps({
            'name': os.uname()[1],                          # 本机名称(hostname)
            'nat_type': self._nat_type,                     # nat 类型
            'external_ip': self._external_ip,               # 公网ip
            'external_port': self._external_port,           # 公网port (net_interface_port nat映射后的端口)
            'personnel_channel': personnel_channel_name,    # 专属频道
            'client_version': CLIENT_INFO,                  # 版本信息
        })
        token = generate_token(WEBSOCKET_SECRET, user, timestamp, info=info)
        credentials = Credentials(**{
            'user': user,
            'timestamp': timestamp,
            'info': info,
            'token': token
        })

        self.centrifuge_client = CentrifugeClient(WEBSOCKET_CLIENT_URL, credentials)
        # 专属频道
        self.centrifuge_client.subscribe(personnel_channel_name, **{
            'message_handle': self._ws_personnel_message_handle,
        })
        # 广播通道
        self.centrifuge_client.subscribe(broadcast_channel_name, **{
            'message_handle': self._ws_broadcast_message_handle,
        })
        self.centrifuge_client.connect()

    def run_infinite(self):
        """
        run infinite
        """
        assert self._settings, '请先设置 settings'

        # 检测nat类型
        self._check_nat()

        self._session = lt.session()
        sts = lt.session_settings()
        for k, v in self._settings.iteritems():
            setattr(sts, k, v)

        if not self._net_interface:
            wan_ips = get_wan_ips()
            if wan_ips:
                self._net_interface = wan_ips[0]
            else:
                self._net_interface = '0.0.0.0'
        logger.info('监听 {}:{}'.format(self._net_interface,
                                      self._net_interface_port))
        self._session.listen_on(self._net_interface_port,
                                self._net_interface_port, self._net_interface)

        # 加载有效的torrent文件
        self._add_valid_torrent_file_from_torrent_folder()

        # 开启线程监听weksocket
        thread = threading.Thread(
            target=self._monitor_websocket, name='MONITOR_RESOURCES')
        thread.start()

        # 在主线程中监听libtrorrent事件
        times = 0
        while True:
            alerts = self._session.pop_alerts()

            # 处理 alert
            for alert in alerts:
                # tracker 错误
                if type(alert) == lt.tracker_error_alert:
                    logger.info('==> (tracker_error_alert) msg: {}'.format(
                        alert.message()))

                # 添加torrent
                elif type(alert) == lt.add_torrent_alert:
                    # 参考 https://libtorrent.org/reference-Core.html#torrent_handle
                    try:
                        handle = alert.handle
                        # print_obj(handle)
                        status = handle.status()
                        # print_obj(status)
                        handle.set_max_connections(60)
                        handle.set_max_uploads(-1)

                        file_path = os.path.abspath(handle.name())
                        torrent_info_hash_str = handle.info_hash().__str__()
                        logger.info(
                            '==> (add_torrent_alert) file: {} hash: {}'.format(file_path, torrent_info_hash_str))

                        key = (file_path, torrent_info_hash_str)
                        self._torrents[key] = handle

                    except Exception as e:
                        logger.warning(e)
                        raise

                # 移除torrent
                elif type(alert) == lt.torrent_removed_alert:
                    # 参考 https://libtorrent.org/reference-Alerts.html#torrent_deleted_alert
                    try:
                        torrent_info_hash_str = alert.info_hash.__str__()
                        logger.info(
                            '==> (torrent_removed_alert) hash: {}'.format(
                                torrent_info_hash_str))
                        for key in self._torrents:
                            _, hash_str = key
                            if torrent_info_hash_str == hash_str:
                                self._torrents.pop(key, None)
                                break

                    except Exception as e:
                        logger.warning(e)

                # 绑定端口失败
                elif type(alert) == lt.listen_failed_alert:
                    logger.info('==> (listen_failed_alert) msg: {}'.format(
                        alert.message()))
                    sys.exit(-1)

                else:
                    logger.info('==> ({}) msg: {}'.format(
                        type(alert), alert.message()))

            # 轮询 torrent 状态
            for index, (key, handle) in enumerate(self._torrents.items()):
                file_path, torrent_info_hash_str = key
                status = handle.status()
                # print_obj(status)

                if index == 0:
                    logger.debug('==================== Torrents ====================')
                self._print_status(status)

                if times % 5 == 0:
                    logger.debug('+++++++++++++++++++++ {} Peers +++++++++++++++++++++'.format(status.name))
                    self._print_peer_info(handle.get_peer_info())

            time.sleep(5)
            times += 1
        thread.join(3)
        logger.warning('已停止bt客户端.')
